QEMU-QTest && Libfuzzer源码分析(下) 0x01 TL;DR 续接上文,开始分析libfuzzer
部分的代码。下文的前置知识基本在上文中已经覆盖。
0x02 generic-fuzz 文件在tests/qtest/fuzz/generic_fuzz.c
中,入口函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static void register_generic_fuzz_targets (void ) { fuzz_add_target(&(FuzzTarget){ }); GString *name; const generic_fuzz_config *config; for (int i = 0 ; i < sizeof (predefined_configs) / sizeof (generic_fuzz_config); i++) { config = predefined_configs + i; name = g_string_new("generic-fuzz" ); g_string_append_printf(name, "-%s" , config->name); fuzz_add_target(&(FuzzTarget){ }); } } fuzz_target_init(register_generic_fuzz_targets);
作者没有设置的device
就要自己去设置启动command
,获取command
的主要函数在generic_fuzz_cmdline
中。
接来下看fuzz
的前期准备generic_pre_fuzz
函数(只截取部分重要代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 static void generic_pre_fuzz (QTestState *s) { if (!getenv("QEMU_FUZZ_OBJECTS" )) { usage(); } qts_global = s; dma_regions = g_array_new(false , false , sizeof (address_range)); dma_patterns = g_array_new(false , false , sizeof (pattern)); fuzzable_memoryregions = g_hash_table_new(NULL , NULL ); fuzzable_pci_devices = g_ptr_array_new(); result = g_strsplit(getenv("QEMU_FUZZ_OBJECTS" ), " " , -1 ); for (int i = 0 ; result[i] != NULL ; i++) { printf ("Matching objects by name %s\n" , result[i]); object_child_foreach_recursive(qdev_get_machine(), locate_fuzz_objects, result[i]); } }
先看标感叹号那块代码,经调试,qdev-get-machine()
这块得到的machine
对象为pc-q35-6.0-machine
,如下:
1 2 3 4 5 6 7 8 9 10 pwndbg> p/x *dev $1 = { class = 0x614000005240 , free = 0x7ffff6ccfba0 , properties = 0x61d000098f00 , ref = 0x2 , parent = 0x604000016cd0 } pwndbg> x/1 s dev.class.type.name 0x603000009b20 : "pc-q35-6.0-machine"
继续看object_child_foreach_recursive
这个函数,这个函数比较绕,调用层次也比较多,如果要细说估计都可以成一篇文章了,我就大概理一下思路:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 static int do_object_child_foreach (Object *obj, int (*fn)(Object *child, void *opaque), void *opaque, bool recurse) { GHashTableIter iter; ObjectProperty *prop; int ret = 0 ; g_hash_table_iter_init(&iter, obj->properties); while (g_hash_table_iter_next(&iter, NULL , (gpointer *)&prop)) { if (object_property_is_child(prop)) { Object *child = prop->opaque; ret = fn(child, opaque); if (ret != 0 ) { break ; } if (recurse) { ret = do_object_child_foreach(child, fn, opaque, true ); if (ret != 0 ) { break ; } } } } return ret; }
为什么Object
还有child Object
?两者是什么关系?搞明白这个才能清楚这个函数的具体含义。
我举个例子,就拿前面我们得到的对象pc-q35-6.0-machine
来举例,这个对象类似于一个根节点,定义在pc-q35.c
代码文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 DEFINE_Q35_MACHINE(v6_0, "pc-q35-6.0" , NULL , pc_q35_6_0_machine_options); #define DEFINE_Q35_MACHINE(suffix, name, compatfn, optionfn) \ static void pc_init_##suffix(MachineState *machine) \ { \ pc_q35_init(machine); \ } \ DEFINE_PC_MACHINE(suffix, name, pc_init_##suffix, optionfn) #define DEFINE_PC_MACHINE(suffix, namestr, initfn, optsfn) \ static const TypeInfo pc_machine_type_##suffix = { \ .name = namestr TYPE_MACHINE_SUFFIX, \ .parent = TYPE_PC_MACHINE, \ .class_init = pc_machine_##suffix##_class_init, \ }; \ static void pc_machine_init_##suffix(void) \ { \ type_register(&pc_machine_type_##suffix); \ } \ type_init(pc_machine_init_##suffix)
再看看pc_q35_init
这个函数:
1 2 3 4 5 6 7 8 static void pc_q35_init (MachineState *machine) { q35_host = Q35_HOST_DEVICE(qdev_new(TYPE_Q35_HOST_DEVICE)); object_property_add_child(qdev_get_machine(), "q35" , OBJECT(q35_host)); }
标记1
处的函数从名称可以看出是给object
的property
添加child
,那么根据函数定义的参数名可以断定是给qdev_get_machine()
所得到的object
,也就是pc-q35-6.0-machine
新增一个property
,命名为q35
,子对象为q35-pcihost
。也就是在两个对象间新建一个链接关系,类似于链表,并对这个链表命名。简单来说创建的链条如下:
1 2 3 4 5 ObjectProperty *op; op->name = "q35" ; op->type = "child<q35-pcihost>" ; op->opaque = OBJECT(q35_host);
当然了,这个“根”对象的child
肯定不止这一个。通过对qdev_get_machine()
的交叉引用可以发现mc146818rtc.c
也是它的child
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static void rtc_realizefn (DeviceState *dev, Error **errp) { ISADevice *isadev = ISA_DEVICE(dev); RTCState *s = MC146818_RTC(dev); object_property_add_tm(OBJECT(s), "date" , rtc_get_date); } ISADevice *mc146818_rtc_init (ISABus *bus, int base_year, qemu_irq intercept_irq) { ISADevice *isadev; isadev = isa_new(TYPE_MC146818_RTC); object_property_add_alias(qdev_get_machine(), "rtc-time" , OBJECT(isadev), "date" ); return isadev; }
object_property_add_tm
函数创建的property
的type
类型为struct tm
,object_property_add_alias
函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ObjectProperty * object_property_add_alias(Object *obj, const char *name, Object *target_obj, const char *target_name) { ObjectProperty *op; ObjectProperty *target_prop; target_prop = object_property_find_err(target_obj, target_name, &error_abort); prop_type = g_strdup(target_prop->type); op = object_property_add(obj, name, prop_type, property_get_alias, property_set_alias, property_release_alias, prop); }
object_property_add_alias
所设置的type
为target_obj
中target_name
的type
,也就是ISADevice
对象中名为data
的type
,为struct tm
。结合起来看,总的来说创建的链条如下:
1 2 3 4 5 ObjectProperty *op; op->name = "rtc-time" ; op->type = "struct tm" ; op->opaque = OBJECT(isadev);
下面就简单说一下链条,链条的type
类型有多种,有child<*>
、link<*>
、string
、bool
、struct tm
、uint8
、uint16
、uint32
、uint64
等等,分别对应着父子对象之间的关系类型。定义不同type
的链条有不同的函数,如object_property_add_tm()
对应struct tm
的type
、object_property_add_bool()
对应bool
的type
,等等。但最终都会调用同一个函数object_property_try_add()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ObjectProperty * object_property_try_add(Object *obj, const char *name, const char *type, ObjectPropertyAccessor *get, ObjectPropertyAccessor *set , ObjectPropertyRelease *release, void *opaque, Error **errp) { ObjectProperty *prop; size_t name_len = strlen (name); prop = g_malloc0(sizeof (*prop)); prop->name = g_strdup(name); prop->type = g_strdup(type); prop->get = get; prop->set = set ; prop->release = release; prop->opaque = opaque; g_hash_table_insert(obj->properties, prop->name, prop); return prop; }
这下object
和child object
就搞明白了,继续看do_object_child_foreach()
函数,应该就很明朗了。
这个函数的作用就是,遍历根对象(pc-q35-6.0-machine)的子对象,筛选出type类型是child<*>的子对象,执行回调函数,并且递归遍历子对象继续执行相同的操作,一直循环反复,直到将所有子对象、子对象的子对象…都遍历完后就退出。
根据调试情况可以验证我们的分析:
1 2 3 4 5 6 7 8 9 10 11 pwndbg> x/1 s prop.name 0x6020000440b0 : "rtc-time" pwndbg> x/1 s prop.type 0x6020000440d0 : "struct tm" pwndbg> x/1 s prop.name 0x60200002f8d0 : "q35" pwndbg> x/1 s prop.type 0x60300005f4a0 : "child<q35-pcihost>"
回到generic_fuzz.c
主函数中来,分析一下前面提到的回调函数locate_fuzz_objects()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 static int locate_fuzz_objects (Object *child, void *opaque) { char *pattern = opaque; if (g_pattern_match_simple(pattern, object_get_typename(child))) { object_child_foreach_recursive(child, locate_fuzz_memory_regions, NULL ); if (object_dynamic_cast(OBJECT(child), TYPE_PCI_DEVICE)) { g_ptr_array_remove_fast(fuzzable_pci_devices, PCI_DEVICE(child)); g_ptr_array_add(fuzzable_pci_devices, PCI_DEVICE(child)); } } else if (object_dynamic_cast(OBJECT(child), TYPE_MEMORY_REGION)) { if (g_pattern_match_simple(pattern, object_get_canonical_path_component(child))) { MemoryRegion *mr; mr = MEMORY_REGION(child); if ((memory_region_is_ram(mr) || memory_region_is_ram_device(mr) || memory_region_is_rom(mr)) == false ) { g_hash_table_insert(fuzzable_memoryregions, mr, (gpointer)true ); } } } return 0 ; } static int locate_fuzz_memory_regions (Object *child, void *opaque) { const char *name; MemoryRegion *mr; if (object_dynamic_cast(child, TYPE_MEMORY_REGION)) { mr = MEMORY_REGION(child); if ((memory_region_is_ram(mr) || memory_region_is_ram_device(mr) || memory_region_is_rom(mr)) == false ) { name = object_get_canonical_path_component(child); g_hash_table_insert(fuzzable_memoryregions, mr, (gpointer)true ); } } return 0 ; }
先简要介绍一下两个函数,第一个是object_dynamic_cast()
,该函数是用来判断两者object
是否有父子关系(继承关系)的。由于这里用来判断是否和TYPE_MEMORY_REGION
有继承关系,我查了一下QEMU
目前没有继承自TYPE_MEMORY_REGION
的情况,因此,这里这个函数的作用就是只判断传入的object
是否为TYPE_MEMORY_REGION
对象。
第二个是object_get_canonical_path_component()
,该函数是获取传入的object
与父对象间的链接关系的名字,也就是链条ObjectProperty->name
。
在locate_fuzz_objects
函数中,当匹配到我们要fuzz
的对象字符串时,又开始递归筛选child
子对象,并执行locate_fuzz_memory_regions
回调函数。该回调函数的作用是判断传入的child
对象是否是TYPE_MEMORY_REGION
对象,是就将memory
空间保存。后续又判断该对象是否属于PCI
继承的设备,属于则保存指针。
再往下看,如果匹配的不是我们要fuzz
的对象字符串,而是TYPE_MEMORY_REGION
对象,并且该对象与父对象的链条名称和我们输入的字符串相同的话,则保存memory
空间。
回到最开始的地方来:
1 2 3 object_child_foreach_recursive(qdev_get_machine(), locate_fuzz_objects, result[i]);
短短一行就展开了这么多的知识点,简单来概括一下前面所讲述的内容。
根据传入的QEMU_FUZZ_OBJECTS的值,假设传入了“virtio*”,那么该函数就是从“根”对象machine开始,不断的循环遍历子对象(继承对象),筛选出名称与“virtio*”相匹配的对象,或者是在一对继承关系中继承链条的名称与“virtio*”相匹配的子对象(且该子对象必须为TYPE_MEMORY_REGION),取出这些对象的MEMORY_REGION区域并保存,也相应的保存满足上述条件而且又属于PCI设备的对象。
结合上手调试,理解的会更快一些。拿virtio-vga
设备举例,我想要fuzz
该设备,传入的QEMU_FUZZ_OBJECT
的值为virtio*
,那么当QEMU
启动后,会匹配所有virtio*
相关的对象或者memory region
,其中,vga-pci.c
文件中有这么一条:
1 2 memory_region_init_io(&subs[0 ], owner, &pci_vga_ioport_ops, s, "vga ioports remapped" , PCI_VGA_IOPORT_SIZE);
也就是会与memory_region
构建一条名为vga ioports remapped
的关系链条。当然还有许多其他条相关的链,经调试得出的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * vga ioports remapped[0 ] (size 20 ) * qemu extended regs[0 ] (size 8 ) * virtio-pci-device[0 ] (size 1000 ) * vga[4 ] (size 1 ) * msix-pba[0 ] (size 8 ) * vga[2 ] (size 10 ) * bochs dispi interface[0 ] (size 16 ) * virtio-pci-notify[0 ] (size 1000 ) * vga[0 ] (size 2 ) * bus master[0 ] (size 0 ) * msix-table[0 ] (size 30 ) * vga-lowmem[0 ] (size 20000 ) * virtio-pci-common[0 ] (size 800 ) * virtio-pci-notify-pio[0 ] (size 4 ) * vbe[0 ] (size 4 ) * bus master container[0 ] (size 0 ) * virtio-vga-msix[0 ] (size 1000 ) * vga[3 ] (size 2 ) * virtio-pci-isr[0 ] (size 800 ) * virtio-pci[0 ] (size 4000 ) * vga[1 ] (size 1 )
总共会保存这些memory region
。其中就包括了前面我们所说的vga ioports remapped
。
继续往下看generic_pre_fuzz()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static void generic_pre_fuzz (QTestState *s) { printf ("This process will try to fuzz the following MemoryRegions:\n" ); g_hash_table_iter_init(&iter, fuzzable_memoryregions); while (g_hash_table_iter_next(&iter, (gpointer)&mr, NULL )) { printf (" * %s (size %lx)\n" , object_get_canonical_path_component(&(mr->parent_obj)), (uint64_t )mr->size); } if (!g_hash_table_size(fuzzable_memoryregions)) { printf ("No fuzzable memory regions found...\n" ); exit (1 ); } pcibus = qpci_new_pc(s, NULL ); g_ptr_array_foreach(fuzzable_pci_devices, pci_enum, pcibus); qpci_free_pc(pcibus); counter_shm_init(); }
这一块就是收尾工作,将PCI
总线和保存的PCI
设备初始化,总的来说就是做一些fuzz
前的初始化准备工作。重点讲一下回调函数pci_enum()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static void pci_enum (gpointer pcidev, gpointer bus) { PCIDevice *dev = pcidev; QPCIDevice *qdev; int i; qdev = qpci_device_find(bus, dev->devfn); g_assert(qdev != NULL ); for (i = 0 ; i < 6 ; i++) { if (dev->io_regions[i].size) { qpci_iomap(qdev, i, NULL ); } } qpci_device_enable(qdev); g_free(qdev); }
dev->io_regions[]
这个比较重要,是PCI Configuration space
中的六个bar
空间,但是看定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #define PCI_NUM_REGIONS 7 struct PCIDevice { PCIIORegion io_regions[PCI_NUM_REGIONS]; } typedef struct PCIIORegion { pcibus_t addr; #define PCI_BAR_UNMAPPED (~(pcibus_t)0) pcibus_t size; uint8_t type; MemoryRegion *memory; MemoryRegion *address_space; } PCIIORegion;
实际上region
有7
个,为什么上面只遍历了6
个?因为最后一个region
实际上是ROM
空间,前六个是RAM
空间,在这里我们用不到ROM
,因此只遍历六个。具体来看下面的代码,这个代码用于给device
添加option rom
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static void pci_add_option_rom (PCIDevice *pdev, bool is_default_rom, Error **errp) { pci_register_bar(pdev, PCI_ROM_SLOT, 0 , &pdev->rom); } void pci_register_bar (PCIDevice *pci_dev, int region_num, uint8_t type, MemoryRegion *memory) { if (region_num == PCI_ROM_SLOT) { wmask |= PCI_ROM_ADDRESS_ENABLE; } }
基本可以看出来是用于ROM
的。同样的,这几个region
的初始化来源也是pci_register_bar()
函数,不过该函数只是注册了一下,并没有分配到实际的地址,地址指的是换成上述结构体来说的话就是PCIIORegion->addr
。更新地址的函数在pci_update_mapping()
。其中io_regions
的内容都是从MemoryRegion
结构体中迁移过来的。具体怎么注册bar
空间的后续会提到一部分。这一块推荐看这篇文章 。
继续看qpci_iomap()
函数,这个函数信息量比较大,我拓展开来看了好久才明白这个函数是做什么的。简单概括一下就是以QTest
的形式重新分配PCI
设备的bar
地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 QPCIBar qpci_iomap (QPCIDevice *dev, int barno, uint64_t *sizeptr) { QPCIBus *bus = dev->bus; static const int bar_reg_map[] = { PCI_BASE_ADDRESS_0, PCI_BASE_ADDRESS_1, PCI_BASE_ADDRESS_2, PCI_BASE_ADDRESS_3, PCI_BASE_ADDRESS_4, PCI_BASE_ADDRESS_5, }; QPCIBar bar; int bar_reg; uint32_t addr, size; uint32_t io_type; uint64_t loc; g_assert(barno >= 0 && barno <= 5 ); bar_reg = bar_reg_map[barno]; qpci_config_writel(dev, bar_reg, 0xFFFFFFFF ); addr = qpci_config_readl(dev, bar_reg); io_type = addr & PCI_BASE_ADDRESS_SPACE; if (io_type == PCI_BASE_ADDRESS_SPACE_IO) { addr &= PCI_BASE_ADDRESS_IO_MASK; } else { addr &= PCI_BASE_ADDRESS_MEM_MASK; } g_assert(addr); size = 1U << ctz32(addr); if (sizeptr) { *sizeptr = size; } }
这里有两个比较疑惑的点,在标记1
处,先写bar
地址为0xFFFFFFFF
,后读bar
地址,不过当我调试的时候,得到的addr
并不是0xFFFFFFFF
。第二点在标记2
处,ctz32()
是取末尾零的个数,为什么这样就能得到bar
空间的size
?
这两个点可以结合起来一起看。前面我们提到注册io_regions
的函数为pci_register_bar()
。这里我们仍然以virtio-vga
设备为栗子。在virtio-vga.c
初始化设备中存在注册函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 static void virtio_vga_base_realize (VirtIOPCIProxy *vpci_dev, Error **errp) { pci_register_bar(&vpci_dev->pci_dev, 0 , PCI_BASE_ADDRESS_MEM_PREFETCH, &vga->vram); } void pci_register_bar (PCIDevice *pci_dev, int region_num, uint8_t type, MemoryRegion *memory) { PCIIORegion *r; uint32_t addr; uint64_t wmask; pcibus_t size = memory_region_size(memory); uint8_t hdr_type; assert(region_num >= 0 ); assert(region_num < PCI_NUM_REGIONS); assert(is_power_of_2(size)); hdr_type = pci_dev->config[PCI_HEADER_TYPE] & ~PCI_HEADER_TYPE_MULTI_FUNCTION; assert(hdr_type != PCI_HEADER_TYPE_BRIDGE || region_num < 2 ); r = &pci_dev->io_regions[region_num]; r->addr = PCI_BAR_UNMAPPED; r->size = size; r->type = type; r->memory = memory; r->address_space = type & PCI_BASE_ADDRESS_SPACE_IO ? pci_get_bus(pci_dev)->address_space_io : pci_get_bus(pci_dev)->address_space_mem; wmask = ~(size - 1 ); if (region_num == PCI_ROM_SLOT) { wmask |= PCI_ROM_ADDRESS_ENABLE; } addr = pci_bar(pci_dev, region_num); pci_set_long(pci_dev->config + addr, type); if (!(r->type & PCI_BASE_ADDRESS_SPACE_IO) && r->type & PCI_BASE_ADDRESS_MEM_TYPE_64) { pci_set_quad(pci_dev->wmask + addr, wmask); pci_set_quad(pci_dev->cmask + addr, ~0U LL); } else { pci_set_long(pci_dev->wmask + addr, wmask & 0xffffffff ); pci_set_long(pci_dev->cmask + addr, 0xffffffff ); } }
在调试过程当中,上述vga
设备注册的MemoryRegion->size
为0x800000
,再看标记3
处,wmask
的结果为0xff800000
,PCIDevice->wmask
的定义是用于实现R/W
字节,应该是用于标记size
的作用。最终配置空间的内存情况是这样的:
1 2 3 4 5 6 7 8 pwndbg> x/10 xg 0x62100002bd00 0x62100002bd00 : 0x0010000010501af4 0x0000000003000001 0x62100002bd10 : 0x0000000000000008 0x000000000000000c 0x62100002bd20 : 0x0000000000000000 0x11001af400000000 0x62100002bd30 : 0x0000009800000000 0x0000010000000000 0x62100002bd40 : 0x0000000201100009 0x0000080000001000
回到qpci_iomap()
函数,当调用标记1
处的函数qpci_config_writel()
时,最终会调用hw/pci/pci.c:pci_default_write_config()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void pci_default_write_config (PCIDevice *d, uint32_t addr, uint32_t val_in, int l) { uint32_t val = val_in; for (i = 0 ; i < l; val >>= 8 , ++i) { uint8_t wmask = d->wmask[addr + i]; uint8_t w1cmask = d->w1cmask[addr + i]; assert(!(wmask & w1cmask)); d->config[addr + i] = (d->config[addr + i] & ~wmask) | (val & wmask); d->config[addr + i] &= ~(val & w1cmask); } }
关键读写的操作在标记4
处。简单来说就是设置bar
空间的地址。根据前面我们知道wmask
的结果是0xff800000
,那么最终设置的bar
地址就是0xff800000
,内存布局如下:
1 2 3 4 5 6 7 8 pwndbg> x/10 xg 0x62100002bd00 0x62100002bd00 : 0x0010000010501af4 0x0000000003000001 0x62100002bd10 : 0x00000000ff800008 0x000000000000000c 0x62100002bd20 : 0x0000000000000000 0x11001af400000000 0x62100002bd30 : 0x0000009800000000 0x0000010000000000 0x62100002bd40 : 0x0000000201100009 0x0000080000001000
这也就是为什么前面我们在qpci_iomap()
函数标记1
先写后读bar
地址为什么会出现不是0xffffffff
的情况。最终得到的地址是0xff800008
。
第一个疑惑点解决了,再来看第二个疑惑点,为什么1U << ctz32(addr);
就能获得size
。当去掉addr
地址的type
执行到标记2
处的时候,addr
的值是0xff800000
,ctz32(addr)
得到的就是23
,1<<23
就是size
,即0x800000
,和最开始注册时的size
是一样的。
根据定义,memory space bar
和I/O space bar
的地址分别是16byte
和4byte
对齐的:
这就是为什么能按上述来获取size
的原因,以及为什么能够利用wmask
来做辅助。还不清楚可以自己动手算一下….
qpci_iomap()
前半部分算是分析完了,再来看剩余的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 QPCIBar qpci_iomap (QPCIDevice *dev, int barno, uint64_t *sizeptr) { } else { loc = QEMU_ALIGN_UP(bus->mmio_alloc_ptr, size); bus->mmio_alloc_ptr = loc + size; qpci_config_writel(dev, bar_reg, loc); } bar.addr = loc; return bar; }
就举例mmio
的情况,pio
其实一样。最开始的bus->mmio_allco_ptr
为0xE0000000
:
1 2 3 4 5 6 7 void qpci_init_pc (QPCIBusPC *qpci, QTestState *qts, QGuestAllocator *alloc) { qpci->bus.pio_alloc_ptr = 0xc000 ; qpci->bus.mmio_alloc_ptr = 0xE0000000 ; qpci->bus.mmio_limit = 0x100000000 ULL; }
后半部分其实就是从地址0xE0000000
开始往后的空间依次分配给PCI
的bar
空间。 qpci_iomap()
函数到这里分析就结束了。
再次回到pci_enum()
函数中来,最后剩下的qpci_device_enable()
函数就是写PCI
设备的配置空间COMMAND
内容处,使得PCI
设备能够正式启用。
至此,正式fuzz
前的准备工作函数generic_fuzz()
都已经分析完毕,所有细节部分我们都已经了解过了。剩下的就只有正式fuzz
函数以及变异策略函数了。
正式fuzz
函数我就不细说,主要就是取libfuzzer
的随机输入数据做拆分并设置几个opcode
做选择。举个栗子,现在有这么一串随机输入数据:
00 01 02 FF 03 04 05 06 FF 01 FF
我设置了几个opcode
函数:
1 2 3 4 5 OP_IN, OP_OUT, OP_READ, OP_WRITE ...
以0xFF
做分割符,分别取出来作为data
和data_len
,那么这一串数据就分别对应一下函数:
1 2 3 * 00 01 02 -> op00 (0102 ) -> in (0102 , 2 ) * 03 04 05 06 -> op03 (040506 ) -> write (040506 , 3 ) * 01 -> op01 (-,0 ) -> out (-,0 )
这就是主fuzz
函数的核心思想。
这里我们再看一下获取mmio
和pio
地址的关键函数get_io_address()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 struct get_io_cb_info { int index; int found; address_range result; }; typedef struct { ram_addr_t addr; ram_addr_t size; } address_range; static bool get_io_address (address_range *result, AddressSpace *as, uint8_t index, uint32_t offset) { FlatView *view; view = as->current_map; g_assert(view); struct get_io_cb_info cb_info = { }; cb_info.index = index; do { flatview_for_each_range(view, get_io_address_cb , &cb_info); } while (cb_info.index != index && !cb_info.found); *result = cb_info.result; if (result->size) { offset = offset % result->size; result->addr += offset; result->size -= offset; } return cb_info.found; } void flatview_for_each_range (FlatView *fv, flatview_cb cb , void *opaque) { FlatRange *fr; assert(fv); assert(cb); FOR_EACH_FLAT_RANGE(fr, fv) { if (cb(fr->addr.start, fr->addr.size, fr->mr, opaque)) break ; } } static int get_io_address_cb (Int128 start, Int128 size, const MemoryRegion *mr, void *opaque) { struct get_io_cb_info *info = opaque ; if (g_hash_table_lookup(fuzzable_memoryregions, mr)) { if (info->index == 0 ) { info->result.addr = (ram_addr_t )start; info->result.size = (ram_addr_t )size; info->found = 1 ; return 1 ; } info->index--; } return 0 ; }
阅读上面的代码需要理解QEMU
的内存管理机制,不明白的推荐看这两篇,QEMU对虚拟机的内存管理 、QEMU内存模型 。
简单说就是随机选取前面保存起来的MemoryRegion
中的一块,进行后续的读写操作。
但是,这里有读者可能就有疑问了,如果要随机选的话,为什么不直接在存储起来的空间中直接挑呢?而是大费周章的去全局遍历,对比,然后再挑选?
因为我们最终目的是要得到MemoryRegion
中的地址、以及size
。虽然说看了MemoryRegion
的结构体后能够发现其本身就有一个addr
属性,但是,这个addr
并不是真正意义上的内存地址,还是一个相对偏移,即类似于offset
。在QEMU
内存管理中,FlatRange
中有指向所属MemoryRegion
的指针,其中也保存着addr
和size
,这里的addr
才是MemoryRegion
真正的地址,具体结构体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct AddrRange { Int128 start; Int128 size; }; struct FlatRange { MemoryRegion *mr; hwaddr offset_in_region; AddrRange addr; uint8_t dirty_log_mask; bool romd_mode; bool readonly; };
这下就明白为什么要“大费周章”的去遍历所有Region
得到addr
了吧。具体的细节看完上面两篇文章后就能弄清楚整一个内存管理的原理了。
最后就是变异策略的函数了,这一块作者写的比较简单,自行阅读代码即可。不过我注意到作者还写了fuzz_dma_read_cb()
函数,但是并没有应用过,觉得没有用吗?这一块读者感兴趣的话可以自己阅读以下,看看具体作用到底是什么。
源码分析到这里就完全结束了,感谢阅读。有错误欢迎斧正。
0x03 Summary 读到这里,我想聪明的读者应该已经发现,其实generic_fuzz
是一个比较dumb
的fuzzer
,优点就在能够通用fuzz
。而且因为受限于QTest
,只有部分设备做了QTest
化的处理,所以能够测试的目标有限,我猜测这也是为什么作者只写了针对virtio
的几个设备写了特定的fuzz
,因为官方在QTest
中只写了virtio
的一部分。如果想要更高效率的fuzz
的话,那还是得需要自己做优化的,我这里仅提供几个思路。
在generic_fuzz
中做结构化fuzz
,也就是争对某个device
做相应的结构体输入化,这样可以充分利用该fuzz
的fork
的优势。缺点是目标单一,不能多设备fuzz
。这里也可以删除他本身的几个opcode
函数,例如读写config
的可以删除,对于我们来说基本没什么用,可以只保留读写mmio/pio
区域的函数,这样可以提高fuzz
效率。
自己编写QTest
化的设备代码,并后续针对这个继续写相应的fuzz
。也就是承接上面官方只写了一部分的情况。这是个体力活,不过我个人估计产出会比较明显。
在generic_fuzz
上做优化(不是单一化),例如提高覆盖率等操作,我自认为它本身还有比较大的改进空间,具体怎么改,我还没有可行的思路,知道的读者麻烦交流一下:)
。
0x04 Reference
https://www.cnblogs.com/ccxikka/p/9477530.html
https://richardweiyang-2.gitbook.io/kernel-exploring/00-kvm/01-memory_virtualization/01_1-qemu_memory_model
https://qemu.readthedocs.io/en/latest/devel/qgraph.html#qgraph
https://blog.csdn.net/weixin_43780260/article/details/104410063